Project Visualizations

Here are some sample plots using the voting data. Any thoughts about what this tells us for analysis?

Setup

Note the setup variables below. These are supposed to be controls all the plots. Sometimes they are. Sometimes not. Have to clean this up, but the goal is uniformity across all the plots.

For fonts, I tried to use ‘sans’ which should pick the system sans serif font for whatever OS is running. In the plotly plot I had to pick a specific font or it defaults to Times New Roman. I picked Arial, but we should look at including Helvetica for macOS users.

# --- Load libraries ---
library(ggplot2)
library(ggforce)
library(dplyr)
library(knitr)
library(readr)
library(sf)
library(tigris)
library(plotly)

# --- Global color palette (Civic Triangle style) ---
fill_col   <- "#ffffff"   # white background
line_col   <- "#3a5f7d"   # roads / accents
text_col   <- "#2f3b44"   # text
border_col <- "#e6eef5"   # county borders (light blue-grey)

# --- Load Texas county-level data ---
turnout <- read_csv("tx_county_voting_data_2020.csv")

# --- Compute voter turnout rate ---
# Adds a new variable Turnout_Rate as a percentage
turnout <- turnout %>%
  mutate(
      Turnout_Rate = (Total_Votes_Cast / Registered_Voters) * 100
    )
  
  # --- Preview the first few rows to confirm ---
  head(turnout)
# A tibble: 6 × 5
  County    COG    Registered_Voters Total_Votes_Cast Turnout_Rate
  <chr>     <chr>              <dbl>            <dbl>        <dbl>
1 Anderson  ETCOG              29274            19227         65.7
2 Andrews   PBDC               10272             5863         57.1
3 Angelina  DETCOG             53166            34574         65.0
4 Aransas   CCRPC              18306            12290         67.1
5 Archer    NCTCOG              6538             4796         73.4
6 Armstrong PHRC                1498             1112         74.2

Figure 1

library(ggplot2)
library(ggforce)

# --- Triangle node coordinates ---
triangle <- data.frame(
  label = c("Education", "Health", "Voter Turnout"),
  x = c(0, 1, 0.5),
  y = c(0, 0, sqrt(3)/2)
)

# --- Arrows for feedback loops ---
arrows <- data.frame(
  x = c(0.5, 1, 0),
  y = c(sqrt(3)/2, 0, 0),
  xend = c(1, 0, 0.5),
  yend = c(0, 0, sqrt(3)/2)
)

# --- Blue-grey palette ---
fill_col <- "#e6eef5"
line_col <- "#3a5f7d"
text_col <- "#2f3b44"

# --- Plot ---
p <- ggplot() +
  geom_polygon(data = triangle, aes(x, y),
               fill = fill_col, color = line_col, linewidth = 1.2) +
  geom_point(data = triangle, aes(x, y),
             size = 6, color = line_col) +
  geom_text(data = triangle, aes(x, y, label = label),
            vjust = c(1.5, 1.5, -2.2),  # pull top label down slightly
            size = 5, family = "sans",
            fontface = "bold", color = text_col) +
  geom_curve(data = arrows, aes(x = x, y = y, xend = xend, yend = yend),
             curvature = -0.25,
             arrow = arrow(length = unit(0.3, "cm")),
             color = line_col, linewidth = 0.8) +
  annotate("text", x = 0.5, y = 0.4,
           label = "Mutual Reinforcement\nand Feedback",
           color = text_col, size = 4.2, family = "sans", lineheight = 1.2) +
  theme_void() +
  coord_equal(xlim = c(-0.2, 1.2), ylim = c(-0.2, 1.05), clip = "off") +
  theme(
    plot.margin = margin(50, 50, 50, 50),  # extra space all around
    plot.title = element_text(
      family = "sans", face = "bold", size = 16,
      hjust = 0.5, color = text_col
    )
  ) +
  ggtitle("The Civic Triangle: Education, Health, and Voter Turnout")

# --- Display inline ---
p

# --- Export PNG with full bounding box ---
ggsave("civic_triangle_final.png", plot = p, width = 9, height = 8,
       dpi = 300, units = "in", limitsize = FALSE)
Figure 1: The Civic Triangle linking Education, Health, and Voter Turnout as mutually reinforcing dimensions of civic vitality.

Figure 2

This chart shows name and turn out on hover.

The interesting thing to note is that the exurbs have higher turnout than urban cores, but the most rural areas have the lowest turnout. Might need to do some math on this based on classifying counties as rural, urban, and suburban to see what shakes out.

# --- Civic Triangle palette (non-data elements) ---
fill_col   <- "#ffffff"   # white background
line_col   <- "#3a5f7d"   # blue-grey for roads and outlines
text_col   <- "#2f3b44"   # text and titles
border_col <- "#e6eef5"   # light blue-grey for county borders

# --- Load Texas county geometries ---
options(tigris_use_cache = TRUE)
tx_counties <- counties(state = "TX", cb = TRUE, class = "sf") %>%
  mutate(County = gsub(" County", "", NAME))

# --- Merge turnout and compute quintiles ---
tx_map <- tx_counties %>%
  left_join(turnout, by = "County") %>%
  mutate(quintile = ntile(-Turnout_Rate, 5))

# --- Load and simplify major roads ---
roads <- primary_roads(class = "sf")
roads_tx <- st_intersection(roads, st_union(st_geometry(tx_counties))) %>%
  st_cast("LINESTRING") %>%
  st_coordinates() %>%
  as.data.frame() %>%
  rename(lon = X, lat = Y)

# --- Quintile fill palette (yellow → red) ---
palette_turnout <- rev(c("#8b0000", "#d73a1f", "#f97c18", "#ffb94e", "#fff5a1"))

# --- Build ggplot ---
p_map <- ggplot() +
  geom_sf(
    data = tx_map,
    aes(
      fill = factor(quintile),
      text = paste0(
        "<b>", County, " County</b><br>",
        "Turnout Rate: ", round(Turnout_Rate, 1), "%"
      )
    ),
    color = border_col, linewidth = 0.25
  ) +
  geom_path(
    data = roads_tx,
    aes(x = lon, y = lat, group = L1),
    color = adjustcolor(line_col, alpha.f = 0.4),
    linewidth = 0.4
  ) +
  scale_fill_manual(
    values = palette_turnout,
    breaks = c("1", "2", "3", "4", "5"),
    labels = c("Highest Turnout", "", "", "", "Lowest Turnout"),
    name = NULL,
    drop = FALSE
  ) +
  coord_sf() +
  theme_void() +
  theme(
    plot.background   = element_rect(fill = fill_col, color = NA),
    panel.background  = element_rect(fill = fill_col, color = NA),
    legend.background = element_rect(fill = fill_col, color = NA),
    legend.position   = "right",
    legend.direction  = "vertical",
    legend.justification = "center",
    legend.key.width  = unit(0.5, "cm"),
    legend.key.height = unit(0.8, "cm"),
    legend.text  = element_text(family = "Arial", color = text_col, size = 10),
    plot.title  = element_text(
      family = "Arial", face = "bold", size = 16,
      hjust = 0.5, color = text_col
    ),
    plot.margin = margin(40, 40, 40, 40)
  ) +
  ggtitle("Texas County Voter Turnout, 2020")

# --- Convert to interactive Plotly map ---
p_map_interactive <- ggplotly(p_map, tooltip = "text") %>%
  layout(
    font = list(family = "Arial, Helvetica, sans-serif", color = text_col),
    paper_bgcolor = fill_col,
    plot_bgcolor = fill_col,
    title = list(
      text = "<b>Texas County Voter Turnout, 2020</b>",
      font = list(family = "Arial, Helvetica, sans-serif", size = 18, color = text_col),
      x = 0.5, xanchor = "center"
    ),
    legend = list(
      orientation = "v",
      x = 1.02,
      y = 0.5,
      xanchor = "left",
      yanchor = "middle",
      traceorder = "normal",
      font = list(family = "Arial, Helvetica, sans-serif", color = text_col, size = 11)
    )
  )

# --- Fix legend labels in Plotly output (keep all colors visible) ---
for (i in seq_along(p_map_interactive$x$data)) {
  if (!is.null(p_map_interactive$x$data[[i]]$name)) {
    p_map_interactive$x$data[[i]]$name <- switch(
      p_map_interactive$x$data[[i]]$name,
      "1" = "Highest Turnout",
      "5" = "Lowest Turnout",
      "2" = " ",
      "3" = " ",
      "4" = " ",
      p_map_interactive$x$data[[i]]$name
    )
  }
}

p_map_interactive
Figure 2: Interactive choropleth of 2020 Texas county voter turnout with highway overlay. Light yellow = highest turnout; dark red = lowest turnout.

Figure 3

This was mapped to see if it would be reasonable to generate the next radar charts using COGs. It would be reasonable for some areas such as Houston, DFW and San Antonio, but not for all regions.

#| label: fig-texas-cog-map
#| fig-cap: "Texas Councils of Governments (COGs) with major highways shown in blue-grey. Boundaries follow official regional divisions used for regional planning and coordination."
#| fig-width: 9
#| fig-height: 8
#| echo: TRUE
#| message: FALSE
#| warning: FALSE

# --- Civic Triangle palette (standard colors) ---
fill_col   <- "#ffffff"   # white background
line_col   <- "#3a5f7d"   # blue-grey lines and accents
text_col   <- "#2f3b44"   # dark blue-grey text
border_col <- "#e6eef5"   # light blue-grey for boundaries

# --- Load Texas counties and merge with COG assignments ---
options(tigris_use_cache = TRUE)
tx_counties <- counties(state = "TX", cb = TRUE, class = "sf") %>%
  mutate(County = gsub(" County", "", NAME))
Retrieving data for the year 2024
# Read your existing CSV (with COG column added)
turnout_with_cog <- turnout

# Merge county geometries with COGs
tx_cog_map <- tx_counties %>%
  left_join(turnout_with_cog, by = "County") %>%
  filter(!is.na(COG)) %>%
  st_as_sf()

# --- Load and clip Texas primary roads ---
roads <- primary_roads(class = "sf")
Retrieving data for the year 2024
roads_tx <- st_intersection(roads, st_union(st_geometry(tx_counties)))
Warning: attribute variables are assumed to be spatially constant throughout
all geometries
# --- Assign a distinct qualitative color palette for 24 COGs ---
# Choose distinct hues, all harmonious with the Civic palette
cog_palette <- c(
  "#e15759", "#f28e2b", "#edc948", "#59a14f", "#76b7b2", "#4e79a7",
  "#9c755f", "#af7aa1", "#ff9da7", "#c49c94", "#8cd17d", "#b6992d",
  "#499894", "#86bcb6", "#fabfd2", "#e15759", "#79706e", "#bab0ab",
  "#d37295", "#d4a6c8", "#a0cbe8", "#f1ce63", "#b07aa1", "#ffbe7d"
)

# --- Build map ---
p_cog <- ggplot() +
  geom_sf(
    data = tx_cog_map,
    aes(fill = COG),
    color = border_col, linewidth = 0.3
  ) +
  geom_sf(
    data = roads_tx,
    color = adjustcolor(line_col, alpha.f = 0.5),
    linewidth = 0.4
  ) +
  scale_fill_manual(
    values = cog_palette,
    name = "Council of Government (COG)"
  ) +
  coord_sf() +
  theme_void() +
  theme(
    plot.background   = element_rect(fill = fill_col, color = NA),
    panel.background  = element_rect(fill = fill_col, color = NA),
    legend.background = element_rect(fill = fill_col, color = NA),
    legend.position   = "right",
    legend.direction  = "vertical",
    legend.justification = "center",
    legend.text  = element_text(family = "Arial", color = text_col, size = 9),
    legend.title = element_text(family = "Arial", face = "bold", color = text_col, size = 10),
    plot.title  = element_text(
      family = "Arial", face = "bold", size = 16,
      hjust = 0.5, color = text_col
    ),
    plot.margin = margin(40, 40, 40, 40)
  ) +
  ggtitle("Texas Councils of Governments (COGs)")

p_cog

Table 1: Abbreviations and full names for Texas Councils of Governments (COGs).
Abbreviations and full names for Texas Councils of Governments (COGs).
**Abbreviation** **Full Name / Description**
AAMPO Alamo Area Metropolitan Planning Organization (also serves as the COG)
ACOG Ark-Tex Council of Governments
BVRG Brazos Valley Council of Governments
CAPCOG Capital Area Council of Governments
CCRPC Coastal Bend Council of Governments (formerly Corpus Christi Regional Planning Commission)
CEN-TEX Central Texas Council of Governments
CTCOG Central Texas Council of Governments (same as CEN-TEX, used for clarity)
DETCOG Deep East Texas Council of Governments
ETCOG East Texas Council of Governments
ETC East Texas Council (of Governments)
GCRPC Golden Crescent Regional Planning Commission
H-GAC Houston-Galveston Area Council
LGRC Lower Rio Grande Valley Development Council
NCTCOG North Central Texas Council of Governments
NORTEX Nortex Regional Planning Commission
PBDC Permian Basin Regional Planning Commission
PHRC Panhandle Regional Planning Commission
RPCF Rio Grande Council of Governments (formerly Region 10 Planning Commission)
SETRPC South East Texas Regional Planning Commission
SPPDC South Plains Association of Governments
TML Texas Department of Transportation’s TxDOT Lubbock District (no formal COG; used for this regional grouping)
WCTCOG West Central Texas Council of Governments

Figure 4

This is a radar chart of Turnout_Ratio, but only for the DFW COG. These charts might be interesting for a pick menu in Shiny.

library(fmsb)
library(dplyr)
library(scales)

# --- COGs to visualize ---
target_cogs <- c("NCTCOG", "H-GAC", "AAMPO")

# --- Function to draw a radar chart for a single COG ---
plot_cog_radar <- function(cog_name) {
  cog_data <- turnout %>%
    filter(COG == cog_name) %>%
    arrange(desc(Turnout_Rate))
  
  if (nrow(cog_data) < 3) {
    message(paste("Skipping", cog_name, "- not enough counties for a radar chart"))
    return(NULL)
  }

  # --- Prepare radar data ---
  cog_chart <- cog_data %>%
    select(County, Turnout_Rate, Total_Votes_Cast)

  max_rate <- ceiling(max(cog_chart$Turnout_Rate))
  min_rate <- floor(min(cog_chart$Turnout_Rate))

  radar_data <- data.frame(
    rbind(
      rep(max_rate, nrow(cog_chart)),
      rep(min_rate, nrow(cog_chart)),
      cog_chart$Turnout_Rate
    )
  )
  colnames(radar_data) <- cog_chart$County

  # --- Plot radar chart ---
  par(
    mar = c(2, 2, 4, 2),
    bg = fill_col,
    family = "Arial"
  )

  radarchart(
    radar_data,
    axistype = 1,
    vlabels = cog_chart$County,
    vlcex = 0.8,
    pcol = line_col,
    pfcol = adjustcolor(line_col, alpha.f = 0.15),
    plwd = 2,
    cglcol = border_col,
    cglty = 1,
    cglwd = 0.8,
    axislabcol = text_col,
    caxislabels = seq(min_rate, max_rate, length.out = 5),
    title = paste(cog_name, "County Turnout Radar (2020)")
  )
}

# --- Plot each COG separately ---
par(mfrow = c(2, 2))  # up to 4 plots per page
for (cog in target_cogs) {
  plot_cog_radar(cog)
}
Figure 3: Radar charts of voter turnout rates for three Texas Councils of Governments (COGs): NCTCOG, H-GAC, and AAMPO. Each plot shows county-level turnout rates.

Figure 5

This chart is not useful, so I did not fix it up, but I left it in anyway.

#| label: fig-texas-dual-axis
#| fig-cap: "Texas counties sorted by total registered voters. Bars show voter turnout percentage; the red line shows total registered voters (right axis)."
#| fig-width: 12
#| fig-height: 7
#| echo: TRUE
#| message: FALSE
#| warning: FALSE

library(ggplot2)
library(dplyr)
library(scales)

# --- Sort all Texas counties by registered voters ---
tx_dual <- turnout %>%
  arrange(desc(Registered_Voters)) %>%
  mutate(County = factor(County, levels = County))

# --- Build dual-axis chart ---
ggplot(tx_dual, aes(x = County)) +

  # --- Left axis: Turnout percentage (bars) ---
  geom_col(aes(y = Turnout_Rate),
           fill = adjustcolor(line_col, alpha.f = 0.6),
           color = border_col,
           width = 0.6) +

  # --- Right axis: Registered voters (scaled line) ---
  geom_line(aes(y = Registered_Voters / max(Registered_Voters) * max(Turnout_Rate)),
            color = "#d73a1f", linewidth = 1.1, group = 1) +
  geom_point(aes(y = Registered_Voters / max(Registered_Voters) * max(Turnout_Rate)),
             color = "#d73a1f", size = 1.8) +

  # --- Axes ---
  scale_y_continuous(
    name = "Voter Turnout (%)",
    sec.axis = sec_axis(
      ~ . / max(tx_dual$Turnout_Rate) * max(tx_dual$Registered_Voters),
      name = "Total Registered Voters",
      labels = label_comma()
    )
  ) +
  scale_x_discrete(expand = expansion(add = 0.5)) +

  # --- Theme ---
  theme_minimal(base_family = "Arial") +
  theme(
    axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1, size = 7, color = text_col),
    axis.text.y.left  = element_text(color = text_col),
    axis.text.y.right = element_text(color = "#d73a1f"),
    axis.title.y.left  = element_text(color = text_col, face = "bold"),
    axis.title.y.right = element_text(color = "#d73a1f", face = "bold"),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank(),
    plot.title = element_text(family = "Arial", face = "bold",
                              hjust = 0.5, size = 16, color = text_col),
    plot.background = element_rect(fill = fill_col, color = NA),
    panel.background = element_rect(fill = fill_col, color = NA),
    plot.margin = margin(30, 30, 30, 30)
  ) +
  ggtitle("Texas County Voter Turnout vs. Registered Voters (2020)")

Notes

Certain elements of this preparation were enhanced with an LLM including but not limited to code restructuring, commenting, and information layout.